feat: configure Enterprise SSO for Replicated VM deployments - #1116
feat: configure Enterprise SSO for Replicated VM deployments#1116jpelletier1 wants to merge 11 commits into
Conversation
aivong-openhands
left a comment
There was a problem hiding this comment.
Reviewed against the OpenHands-Cloud Helm chart guidelines. The Keycloak auto-provisioning logic itself is solid, but there's an important configuration-naming problem that will bite self-hosted consumers.
Important - two near-identical values keys (enterpriseSso vs enterpriseSSO)
This PR adds a new key enterpriseSso (lowercase sso) at charts/openhands/values.yaml:117, but the chart already ships enterpriseSSO (capital SSO) at charts/openhands/values.yaml:224 (added in #325). YAML keys are case-sensitive, so these are two independent toggles that differ only by capitalization:
enterpriseSSO.enabled(existing) ->templates/_env.yamluses.Values.enterpriseSSO.enabledto add"enterprise_sso"toOH_WEB_CLIENT_PROVIDERS_CONFIGURED.enterpriseSso.enabled+idpMetadataUrl(new) -> gates the Keycloak SAML IdP auto-provisioning inkeycloak-config-script.yaml.
Having enterpriseSso and enterpriseSSO side by side in the same values.yaml is a serious foot-gun: a consumer who sets one will almost certainly assume they set the other. This runs against the "consumer-friendly configuration / one knob, one effect" principle. I'd strongly suggest consolidating onto the existing enterpriseSSO key (e.g. add displayName and idpMetadataUrl under it) so a single toggle drives both the web-client provider advertisement and the Keycloak auto-config.
Important - consumer split-brain for raw-Helm installs
Because of the two keys, enabling SSO end-to-end via the raw chart currently requires setting both enterpriseSSO.enabled: true (so the provider is advertised to the web client) and enterpriseSso.enabled: true + idpMetadataUrl (so Keycloak is configured). The new README.md section only documents the lowercase enterpriseSso key, so a consumer who follows it will get Keycloak provisioned but the provider may never be advertised to the web client. Please make the documented path a single, complete toggle.
Related: replicated/openhands.yaml wires the new lowercase enterpriseSso block and ENABLE_ENTERPRISE_SSO, but never sets the capital enterpriseSSO, so OH_WEB_CLIENT_PROVIDERS_CONFIGURED won't include enterprise_sso on Replicated installs.
Question - ENABLE_ENTERPRISE_SSO vs OH_WEB_CLIENT_PROVIDERS_CONFIGURED
The button appears to be driven by ENABLE_ENTERPRISE_SSO (presence-checked, set only in the Replicated layer), while OH_WEB_CLIENT_PROVIDERS_CONFIGURED is driven by the capital enterpriseSSO.enabled. Are both needed? Note that for pure-Helm consumers nothing sets ENABLE_ENTERPRISE_SSO, so it would help to clarify which mechanism is authoritative and ensure the raw chart covers it.
Looks good (no action needed)
- Failure-safety is correct: the provisioning runs in a subshell terminated by
|| echo "WARNING...", so an unreachable/invalid metadata URL warns without blocking pod startup, and the internalexit 1only exits the subshell. validateSignatures: "true",trustEmaildocumented, andhideOnLogin: true(valid as a top-level field on the bundled Keycloak - bitnami chart 24.7.5) are all sensible defaults.- The upsert style (existence check -> PUT/POST,
keycloak_api_call) matches the existing realm/IdP/mapper loop vocabulary, and a separate block is justified since the SAML metadata is imported dynamically and can't live in the static realm template. - The Replicated env mapping that renders
truevs an empty string (rather than the literalfalse) correctly matches the app's presence-check semantics and mirrors the existing conditional-literal pattern in this file.
This review was generated by an AI agent (OpenHands) on behalf of the user.
Adds an 'Enterprise SSO (SAML) Authentication' group to the KOTS config screen with a single toggle, enterprise_sso_enabled. When enabled, the HelmChart values map sets ENABLE_ENTERPRISE_SSO=true on the OpenHands application server, which surfaces the 'Connect to Enterprise SSO' button on the login page. The env mapping uses an if/end template that renders 'true' or '': the backend and web-client config injector only presence-check the variable (strip + truthiness), so rendering the literal string 'false' would still enable the feature. Template style mirrors the existing conditional informers in application.yaml and postgres conditional values in openhands.yaml. The toggle only controls UI visibility; operators still need to create the enterprise_sso SAML/OIDC identity provider in the bundled Keycloak (allhands realm) for sign-in to work — documented in help_text. Co-authored-by: openhands <openhands@all-hands.dev>
Extends the Replicated enterprise_sso_authentication group with two optional fields shown when the toggle is on: - Identity Provider Display Name (default 'Company SSO') - SAML Metadata URL (blank = manage Keycloak manually) The HelmChart values map now passes an enterpriseSso block (enabled/displayName/idpMetadataUrl) into the openhands chart. In the chart's keycloak-config init container (the idempotent realm provisioning script that runs on every pod start), a new gated block: - POSTs the metadata URL to Keycloak's identity-provider/import-config endpoint - upserts the enterprise_sso SAML IdP (trustEmail, validate signatures, hidden from the Keycloak login page; reachable via the app's kc_idp_hint=enterprise_sso hint) - upserts the hardcoded identity_provider=enterprise_sso:saml mapper the app uses to detect SAML logins and skip OAuth-only token handling The block runs after realm create/update and inside a subshell, so a bad or unreachable metadata URL logs a clear error and warns without blocking application startup. Defaults are inert (enabled=false, metadata url empty), so existing installs and OSS values render no behavior change. Schema and README updated; shell script verified with sh -n / bash -n. Co-authored-by: openhands <openhands@all-hands.dev>
Correct the Keycloak import contract, consolidate Enterprise SSO values, secure operator input handling, and cover managed provider lifecycle with regression tests.\n\nCo-authored-by: openhands <openhands@all-hands.dev>
b496556 to
90ca0c2
Compare
Keep the focused Keycloak contract job self-contained by using stdlib parsing and source assertions.\n\nCo-authored-by: openhands <openhands@all-hands.dev>
aivong-openhands
left a comment
There was a problem hiding this comment.
Reviewed at 349b9243.
Verified locally before writing this:
helm unitteston the newenterprise_sso_test.yamlplus the changedvalues_schema_test.yaml: 13 passed.pytest scripts/test_keycloak_realm_template.py: 17 passed, 1 pre-existing failure (test_keycloak_identity_provider_socket_timeout, needs built subcharts, unrelated to this PR).- Rendered
keycloak-config.shfor both the auto-config and the disable branch and ransh -nandbash -non each. Both clean. - Ran the extracted identity-provider
jqfilter through realjqagainst a metadata import response with no signing certificate. - Confirmed the bundled Keycloak is 26.3.0 (bitnami chart 24.7.5), so
hideOnLoginas a top-level field is correct for this version.
Nice work on passing the operator inputs as container env and encoding them with jq --arg. That is the right call, and the jq-filter test is the one new test I would keep as is.
Three things I think are worth changing before this merges.
1. ENABLE_ENTERPRISE_SSO is only set on the Replicated path
replicated/openhands.yaml is the only place it is set. charts/openhands/templates/_env.yaml adds enterprise_sso to OH_WEB_CLIENT_PROVIDERS_CONFIGURED, but nothing sets ENABLE_ENTERPRISE_SSO. The enterprise server also builds its own providers_configured list from ENABLE_ENTERPRISE_SSO, and that path does not read the OH_WEB_CLIENT_* form. So a plain Helm install that sets enterpriseSSO.enabled: true can get the identity provider created in Keycloak and still show no login button.
The chart's existing convention for this lives in _env.yaml: ENABLE_JIRA, ENABLE_JIRA_DC, and ENABLE_LINEAR are all {{- if .Values.<x>.enabled }} blocks with value: "true". Following that here fixes the self-hosted case and lets you drop the repl{{ if }}...repl{{ end }} form.
One related detail: the current form renders value: "" when the toggle is off, not an absent variable. I confirmed the env dedup wrapper emits the empty entry. It is harmless today because the app strips the value before testing it, but the comment's "presence-checked" wording does not match that.
2. validateSignature is forced to "true" even with no signing certificate
The filter ends with config: (. + {"syncMode": "IMPORT", "validateSignature": "true"}), and the import guard only checks idpEntityId and singleSignOnServiceUrl. Running the filter against a certificate-less import response gives:
"config": {
"idpEntityId": "https://idp/e",
"singleSignOnServiceUrl": "https://idp/sso",
"postBindingResponse": "true",
"validateSignature": "true",
"syncMode": "IMPORT"
}No signingCertificate. Keycloak sets validateSignature itself based on what it finds in the descriptor, and this overrides that to true with nothing to validate against. The init container then logs enterprise_sso SAML identity provider configured. and the pod goes Ready, but every login fails at signature verification. Adding has("signingCertificate") to the guard would catch it.
Worth considering instead: set useMetadataDescriptorUrl: "true" and metadataDescriptorUrl and let Keycloak fetch the descriptor itself. That removes the import call and its response validation entirely. It also fixes a second problem. Today the certificate is pinned at pod-start time, so an identity provider certificate rotation breaks SSO until someone restarts the pod.
3. No guard for a metadata URL with no chart-provisioned Keycloak
keycloak-config-script.yaml renders only when openhands.keycloakProvisionRealm is true, which needs keycloak.enabled or keycloak.provisionRealm. With both false and an external Keycloak, idpMetadataUrl passes the schema and silently does nothing. templates/validations.yaml is the established place for this, and the guard is a couple of lines.
Smaller points
The disable path keys off a retained idpMetadataUrl. On Replicated that works, since KOTS keeps a hidden item's stored value, and there is already a comment in replicated/openhands.yaml saying so. For a chart consumer the natural action is to set enabled: false and delete the URL, which skips the branch. The provider stays enabled and kc_idp_hint=enterprise_sso keeps working, which is the bypass the branch exists to prevent. The README asks operators to remember to retain the URL for one rollout, and that is easy to get wrong. What is the reason for gating on idpMetadataUrl rather than reconciling whenever enterpriseSSO.enabled is false? That needs no marker value at all.
Failures are invisible, and the reused token makes them more likely. The subshell swallows everything, the pod goes Ready, and the only signal is one init-container log line. The block also reuses the ACCESS_TOKEN minted before the whole realm sync of clients, identity providers, and mappers. Master admin-cli tokens are short-lived, so a slow realm update turns this into a 401 that fails the import guard, and the result is a healthy-looking deploy with no identity provider. Re-minting the token at the top of the block is cheap. The disable branch has the same issue and is quieter still: a 401 body has no internalId, so it takes the "does not exist" path and prints nothing.
trustEmail: true is hardcoded. That means the SAML assertion's email is trusted for auto-linking to existing accounts, including accounts created through GitHub OAuth, with no verification step. That is a reasonable default for corporate SSO, but it is a trust decision made on the operator's behalf and it is not mentioned in the README or the installer help text.
The "Company SSO" fallback lives in the jq filter while values.yaml has displayName: "". Putting the default in values.yaml, or using | default "Company SSO" in deployment.yaml, makes it visible to chart consumers and gives one source of truth. replicated/config.yaml could then just carry default: "Company SSO".
Some of the new tests assert the diff back to itself. The exact values.yaml key order and formatting, "enterpriseSso" not in ..., "validateSignatures" not in ..., and the exact regex string in config.yaml all guard typos that were never on main, and they break on a harmless reformat. Meanwhile nothing asserts that the ENTERPRISE_SSO_* env vars land on the keycloak-config init container. The pytest test greps template text, and the helm-unittest suite only checks containers[0]. One assert on initContainers[?(@.name=="keycloak-config")].env would catch a real regression, and a certificate-less metadata case for the jq filter would be worth more than the string greps.
Two tiny ones. IMPORT_REQUEST sends alias, which import-config ignores. And the values.schema.json pattern ^$|^https:// is looser than the installer regex, since it accepts a bare https:// and embedded whitespace.
This comment was drafted by an AI agent on behalf of the user.
Co-authored-by: openhands <openhands@all-hands.dev>
|
Thanks for the thorough follow-up. I addressed the actionable items in
Two decisions are intentional:
Verification: 468 script tests and 111 Helm tests pass; Helm lint, Ruff, and rendered This comment was generated by an AI agent (OpenHands) on behalf of the user. |
There was a problem hiding this comment.
🔴 Needs improvement — the overall design is reasonable, but three important failure/operability gaps need attention.
Current status: The inline findings were revalidated on head
a99f064; the merge frommaindid not change the Enterprise SSO implementation.
[CRITICAL ISSUES]
- The best-effort shell wrapper can suppress failed Keycloak operations and report success.
- Init-container Enterprise SSO variables bypass the chart's explicit environment de-duplication/override contract.
- The documented manual setup path omits the mapper this change says OpenHands relies on.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
Defaults are inert and CI is green, but this changes authentication provisioning. The identified paths can leave SSO unconfigured or partially configured while deployment appears healthy.
VERDICT:
❌ Needs rework: Address the three inline issues before merging.
KEY INSIGHT:
Authentication provisioning must fail observably and preserve the chart's established override contract, even when startup remains best-effort.
This review was generated by an AI agent (OpenHands) on behalf of the user.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- When your PR is merged, the guideline file goes through normal code review by repository maintainers.
Resolve with AI? Install the iterate skill in your agent and run
/iterateto automatically drive this PR through CI, review, and QA until it's merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
Co-authored-by: openhands <openhands@all-hands.dev>
aivong-openhands
left a comment
There was a problem hiding this comment.
Reviewed at c1ba37d against the OpenHands-Cloud Helm chart guidelines. All 22 checks are green and I re-verified the shell/jq behaviour locally rather than reading it off the diff.
The overall shape is good: one canonical enterpriseSSO values key, HTTPS enforced in both the JSON schema and the KOTS regex, operator input passed as container env and encoded with jq --arg (I confirmed a display name of Company "Platform" $(touch /tmp/should-not-run) round-trips as data), inert defaults, and a render guard that fails fast when idpMetadataUrl is set without chart-managed realm provisioning. The earlier review rounds clearly moved this a long way.
Two important items remain. Neither blocks the design, but both are the kind of thing that turns into a support ticket rather than a failed deploy.
[IMPORTANT]
-
keycloak_api_callstill reports success for an empty response body, so the SSO write paths can log "Created identity provider: enterprise_sso" when nothing was written. Verified locally:keycloak_api_call "printf ''"returns 0. Because every call usescurl -swithout-for%{http_code}, HTTP status is discarded, and aPUTthat succeeds (204, empty body) is indistinguishable from one that failed with an empty body. The HTML-error-page case is now caught by the new parse guard — good — so this is a narrow but real residual gap.keycloak_get_optional, added in this same PR, already has the right shape; routing the four SSOPOST/PUTcalls through a status-checking helper would close it cheaply and consistently. This matters more here than elsewhere in the script because the SSO block is deliberately best-effort: a masked failure produces a green deploy plus a login button that dead-ends at Keycloak, with no signal anywhere except init-container logs. -
idpMetadataUrlis doing two jobs: it supplies the metadata and marks the provider as chart-managed. The disable branch keys off.Values.enterpriseSSO.idpMetadataUrlalone, so the README has to instruct operators to keep a metadata URL populated while turning SSO off — a value whose name promises "where to fetch metadata" now silently means "the chart owns this provider." That is theone knob, one effectrule in the repo guidelines, and the effect isn't predictable from the key name. It also has a concrete failure mode: if an operator clears the URL in the same rollout that flips the toggle off (the intuitive action, and what the KOTS UI nudges toward since the field is hidden bywhen:), neither branch renders and the provider stays enabled in Keycloak — reachable via a directkc_idp_hint=enterprise_ssorequest, which is exactly what the disable path exists to prevent. An explicit ownership marker (e.g. amanaged-byentry written into the provider'sconfigat create time, or a separate values key) would make the intent legible and survive the URL being cleared. Related question I could not settle from the repo alone: doesConfigOption "enterprise_sso_idp_metadata_url"still return the stored value oncewhen:hides the field? If KOTS returns empty for hidden items, the disable path never renders in Replicated installs — the primary target of this PR. Worth confirming before merge.
[MINOR]
-
"Company SSO"is defaulted in three places (values.yaml, the| defaultindeployment.yaml, andreplicated/config.yaml). Sincevalues.yamlalready sets it, the template fallback only fires when someone explicitly sets it to empty. Dropping the| defaultwould leave one authoritative chart default. -
The two env entries in
deployment.yamlre-implement the.Values.envprecedence contract withhasKeyguards instead of flowing throughopenhands.env. It renders correctly today — I confirmed the override test exercises the realhelm templateoutput — but it is a second copy of a cross-cutting rule that will drift from the helper. If keeping them out ofopenhands.env.defaultsis deliberate (to avoid leaking SSO env into the app and litellm containers), a one-line comment saying so would help the next person.
[RISK] Moderate but well-contained. Defaults are inert, so existing installs and OSS values render unchanged, and the render guard prevents the half-configured combination. The residual exposure is operational rather than structural: a provisioning failure is best-effort by design and, given item 1, may not be visible at all; and item 2's disable path can silently no-op. Both surface as "SSO is broken/still on" rather than a failed rollout, which is the harder class to debug.
Happy to approve once items 1 and 2 are addressed or explicitly dismissed with reasoning.
This review was generated by an AI agent (OpenHands) on behalf of the user.
Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed the latest review in
All 473 script tests, 117 Helm tests, and 22 GitHub checks pass. The four latest review threads have replies and are resolved. This comment was generated by an AI agent (OpenHands) on behalf of the user. |
aivong-openhands
left a comment
There was a problem hiding this comment.
Reviewed at 7c02209 against the OpenHands-Cloud Helm chart guidelines. I rendered the chart and executed the generated script against a stub Keycloak rather than reading behaviour off the diff.
What I verified locally
helm unittestonenterprise_sso_test.yaml,validations_test.yaml,values_schema_test.yaml: 31 passed.pytest scripts/test_keycloak_realm_template.py: 24 passed.- Rendered
keycloak-config.shin automatic, manual, and disabled modes; all passsh -nandbash -n. - Ran the rendered enterprise-SSO block end to end against a stub Keycloak. It POSTs the provider and the mapper, and the stored provider carries
validateSignature=true,syncMode=IMPORT,openhandsManaged=true, andattribute.value=enterprise_sso:saml. Re-ran the toggle-off block against that same stub state and it correctly flippedenabledtofalse.
The earlier round of feedback was genuinely addressed: transport and HTTP status failures now propagate, openhandsManaged replaced the overloaded idpMetadataUrl ownership inference, and the manual mapper contract is documented in both the README and the installer help text. The Keycloak provisioning logic is correct on the happy path.
Two findings below are worth attention before merge. Neither is a blocker for the provisioning logic itself; both are reachable states an operator can land in through the installer UI.
Important — toggling Enterprise SSO off in the installer renders an un-deployable chart.
validations.yaml fails on idpMetadataUrl being non-empty regardless of enterpriseSSO.enabled. But replicated/config.yaml marks enterprise_sso_idp_metadata_url with when: enterprise_sso_enabled == "1", and hiding a KOTS config item does not clear the value the operator already typed. So the natural "turn Enterprise SSO back off" path leaves enabled: false with a stale URL still set.
That combination is fine while keycloak.enabled=true (the Replicated default), but it fails to render on any split-Keycloak deployment. Verified:
$ helm template t charts/openhands --set enabled=true \
--set keycloak.enabled=false --set keycloak.provisionRealm=false \
--set enterpriseSSO.enabled=false \
--set-string enterpriseSSO.idpMetadataUrl=https://idp.example.com/saml/metadata
Error: enterpriseSSO.idpMetadataUrl requires chart-managed Keycloak realm provisioning...
The operator disabled the feature and the deploy now fails on a value they cannot see in the UI. Guarding on the URL only when the feature is on — and .Values.enabled .Values.enterpriseSSO.enabled .Values.enterpriseSSO.idpMetadataUrl (not (include ...)) — matches the intent ("you asked for auto-provisioning but there is no realm to provision into") and makes the toggle-off path inert, which is what the disable branch already assumes.
Important — the disable path is one-way; re-enabling never restores the provider.
The toggle-off branch only sets .enabled = false on the managed provider, which is the right conservative choice. But the re-enable path is and enabled idpMetadataUrl, and per the finding above the metadata URL is the value most likely to be missing after a toggle-off/toggle-on cycle. In that state:
enabled: true, no URL →ENABLE_ENTERPRISE_SSOis set andenterprise_ssois advertised inOH_WEB_CLIENT_PROVIDERS_CONFIGURED,- but neither branch of the script renders, so the disabled provider in Keycloak stays disabled.
I confirmed both halves: rendering with enabled=true, idpMetadataUrl="" emits the login-provider env vars but produces no provisioning and no reconciliation block, and my stub still held enabled: false after the disable run. The result is a login button that leads to a disabled IdP, with no message anywhere explaining why. Re-enabling a provider that already carries openhandsManaged=true is the symmetric counterpart to the disable branch and closes the loop.
The remaining notes are minor and I would not hold the PR for them.
This review was generated by an AI agent (OpenHands) on behalf of the user.
| syncMode: "FORCE" | ||
| } | ||
| }' > /tmp/mapper-enterprise-sso.json || exit 1 | ||
| keycloak_api_call "curl -s \"$KC_REALM/identity-provider/instances/enterprise_sso/mappers\" $AUTH" || exit 1 |
There was a problem hiding this comment.
Minor — the mapper existence check reads a bare RESPONSE and treats every non-array shape as "no mapper", so it falls through to POST. The provider and mapper writes below correctly use the status-aware keycloak_write, but this read still goes through keycloak_api_call, which only inspects the body for an error key and never sees the HTTP status.
Exercised the helper directly with the rendered script's own filter:
200 empty body -> POST -> create
200 whitespace body -> POST -> create
object instead of array -> POST -> create
empty array -> POST -> create
array with the mapper -> PUT (id=abc)
The first three are the interesting rows: a proxy returning an empty 200, or Keycloak answering with an object because the provider POST above silently didn't land, both read as "mapper absent" and re-POST. Keycloak permits duplicate mapper names on the same alias, so a flapping proxy accumulates mappers rather than converging — which undercuts the idempotency the block's own comment promises.
keycloak_get_optional already returns the status-checked body you want here. Swapping this call for it and rejecting a non-array RESPONSE before the select would make the read as safe as the writes. Note that keycloak_get_optional maps 404 to an empty RESPONSE, so the non-array guard needs to distinguish "absent" from "unparseable".
| {{- include "openhands.env" . | nindent 8 }} | ||
| {{- if and .Values.enterpriseSSO.enabled .Values.enterpriseSSO.idpMetadataUrl }} | ||
| {{- /* Keep these init-only; openhands.env already emits .Values.env overrides. */}} | ||
| {{- if not (hasKey (.Values.env | default dict) "ENTERPRISE_SSO_DISPLAY_NAME") }} |
There was a problem hiding this comment.
Minor, and I'm fine with the current answer — just flagging the consequence. These hasKey guards give .Values.env precedence for the init container, which is correct and matches openhands.env. The side effect is that enterpriseSSO.displayName is only ever emitted when idpMetadataUrl is also set.
$ helm template ... --set enterpriseSSO.enabled=true \
--set-string enterpriseSSO.displayName="Acme SSO" \
--set-string enterpriseSSO.idpMetadataUrl=""
# "Acme SSO" appears 0 times in the rendered output
So in manual mode an operator can set displayName in values (or type it into the installer field, which config.yaml shows whenever the toggle is on, independent of the URL) and it is silently discarded — the name comes from whatever they typed into the Keycloak admin console instead. That's the correct runtime behaviour, since the chart isn't managing the provider, but nothing tells them. A sentence in the values.yaml comment saying displayName applies only when idpMetadataUrl is set would cover it.
Description
Adds Replicated Admin Console configuration for Enterprise SSO (SAML) in VM deployments, including optional automatic provisioning of the
enterprise_ssoidentity provider in the bundled Keycloak realm.What changed
enterpriseSSO, for both login-provider advertising and Keycloak provisioning.ImportConfigrequest and consumes its flat configuration-map response.validateSignature=true, trust-email, and the hardcodedidentity_provider=enterprise_sso:samlmapper..Values.envprecedence for Enterprise SSO init-container settings without duplicate environment entries.jq, rather than interpolating them into shell source.idpMetadataUrlis blank.Provisioning remains best-effort: invalid or unreachable metadata logs a warning without blocking OpenHands startup.
Helm Chart Checklist
enabled: false, empty metadata URL).Verification
473 passed— repository script tests117 passed— Helm unit tests, including automatic, manual, disabled, and operator-override SSO modeskeycloak-config.shpassessh -nandbash -nin automatic, manual, and disabled modesb53f613This PR description was updated by an AI agent (OpenHands) on behalf of the user.